package com.wiringweb.servlet;

import com.gooddies.utils.Pair;
import com.wiringweb.exceptions.ExceptionHandler;
import com.wiringweb.exceptions.WiringWebException;
import com.wiringweb.exceptions.WiringWebNotFound;
import com.wiringweb.http.HttpMethod;
import com.wiringweb.json.JsonSerializer;
import com.wiringweb.json.JsonSerializerImpl;
import com.wiringweb.resolution.Resolution;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
import javax.servlet.ServletConfig;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @author Dmitry
 */
public class WiringRequestProcessor {

    private final ThreadLocal<HttpServletRequest> threadLocalRequest = new ThreadLocal<HttpServletRequest>();
    private JsonSerializer jsonSerializer;
    private final Map<HttpMethod, UrlsContainer> methodsToUrlsMap;
    private final Object[] emptyParameterList = new Object[0];
    private final RequestParameterProcessor parameterProcessor;
    private ExceptionHandler exceptionHandler;
    /**
     * Map to get prerequest and postRequest methods that should be executed on
     * this controller
     */
    private Map<BaseWiringWebController, Pair<Method, Method>> controllerToPrePostMethods;
    /**
     * Map to get prerequest and postRequest methods that should be executed on
     * this controller
     */
    private Map<Method, Pair<Method, Method>> methodToPrePostMethods;

    public WiringRequestProcessor(Map<HttpMethod, UrlsContainer> methodsToUrlsMap) {
        this.methodsToUrlsMap = methodsToUrlsMap;
        parameterProcessor = new RequestParameterProcessor();
        jsonSerializer = createJsonSerializer();
    }

    public void setExceptionHandler(ExceptionHandler exceptionHandler) {
        this.exceptionHandler = exceptionHandler;
    }

    void setControllerToPrePostMethods(Map<BaseWiringWebController, Pair<Method, Method>> controllerToPrePostMethods) {
        this.controllerToPrePostMethods = controllerToPrePostMethods;
    }

    void setMethodToPrePostMethods(Map<Method, Pair<Method, Method>> methodToPrePostMethods) {
        this.methodToPrePostMethods = methodToPrePostMethods;
    }

    protected JsonSerializer createJsonSerializer() {
        return new JsonSerializerImpl();
    }

    public JsonSerializer getJsonSerializer() {
        return jsonSerializer;
    }

    private UrlBindWithUrl getUrlBinding(String context, String url, HttpMethod httpMethod) {
        url = url.substring(context.length());
        UrlsContainer container = methodsToUrlsMap.get(httpMethod);
        if (container == null) {
            return null;
        }

        return container.resolveUrl(url);
    }

    private void sendError(int code, String message, HttpServletResponse response) throws IOException {
        response.sendError(code, message);
    }

    private void sendNotFoundError(HttpServletRequest request, HttpServletResponse response) throws IOException {
        sendError(404, "Url " + request.getRequestURI() + " is not found", response);
    }

    public HttpServletRequest getRequest() {
        return threadLocalRequest.get();
    }

    public void processRequest(HttpServletRequest request, HttpServletResponse response, ServletConfig servletConfig) throws Exception {
        threadLocalRequest.set(request);
        HttpMethod httpMethod = HttpMethod.valueOf(request.getMethod());
        UrlBindWithUrl urlBind = getUrlBinding(request.getContextPath(), request.getRequestURI(), httpMethod);
        if (urlBind == null) {
            sendNotFoundError(request, response);
            return;
        }

        UrlBind bind = urlBind.getUrlBind();
        BaseWiringWebController controller = bind.getController();
        Method method = bind.getMethod();
        Pair<Method, Method> controllerPrePostRequestsHandler = controllerToPrePostMethods.get(controller);
        Pair<Method, Method> methodPrePostRequestsHandler = methodToPrePostMethods.get(method);

        Object[] parameters = parameterProcessor.parameterProcessor(urlBind.getUrl(), bind, request, response);
        if (parameters == null) {
            parameters = emptyParameterList;
        }

        Resolution resolution = null;
        boolean handled = false;
        try {
            try {
                //Controller prerequest
                if (controllerPrePostRequestsHandler != null) {
                    Method controllerPrerequest = controllerPrePostRequestsHandler.getFirst();
                    if (controllerPrerequest != null) {
                        Class c = controllerPrerequest.getParameterTypes()[0];
                        if (c == HttpServletRequest.class) {
                            controllerPrerequest.invoke(controller, request);
                        } else {
                            controllerPrerequest.invoke(controller, response);
                        }
                    }
                }
                if (methodPrePostRequestsHandler != null) {
                    Method methodPrerequest = methodPrePostRequestsHandler.getFirst();
                    if (methodPrerequest != null) {
                        methodPrerequest.invoke(controller, request);
                    }
                }

                resolution = (Resolution) method.invoke(controller, parameters);

                if (methodPrePostRequestsHandler != null) {
                    Method methodPostrequest = methodPrePostRequestsHandler.getSecond();
                    if (methodPostrequest != null) {
                        methodPostrequest.invoke(controller, request, resolution);
                    }
                }

                if (controllerPrePostRequestsHandler != null) {
                    Method controllerPostrequest = controllerPrePostRequestsHandler.getSecond();
                    if (controllerPostrequest != null) {
                        controllerPostrequest.invoke(controller, request, resolution);
                    }
                }
            } catch (IllegalAccessException ex) {
                throw new WiringWebException("Error while invoke method " + method.getName() + " of controller " + controller.getClass().getName(), ex);
            } catch (IllegalArgumentException ex) {
                throw new WiringWebException("Error while invoke method " + method.getName() + " of controller " + controller.getClass().getName(), ex);
            } catch (InvocationTargetException ex) {
                throw new WiringWebException("Error while invoke method " + method.getName() + " of controller " + controller.getClass().getName(), ex);
            } catch (WiringWebNotFound ex) {
                sendError(500, ex.getMessage(), response);
            }
        } catch (Exception ex) {
            if (!tryToHandleException(ex, controller, request, response, servletConfig)) {
                throw ex;
            } else {
                handled = true;
            }
        }

        if (!handled) {
            if (resolution == null) {
                sendError(500, "Method " + method.getName() + " in controller " + controller.getClass().getName() + " returned null resolution", response);
            } else {
                resolution.process(request, response, this, servletConfig);
            }
        }
    }

    private boolean tryToHandleException(Throwable ex, Object controller, HttpServletRequest request, HttpServletResponse response, ServletConfig servletConfig) throws Exception {
        Resolution resolution = exceptionHandler.processException(ex, controller, request, response);
        if (resolution == null) {
            return false;
        }

        resolution.process(request, response, this, servletConfig);
        return true;
    }
}
